Skip to content

fix(realtime): 批量创建/删除日程时连续对话被强制挂断,且本地日历静默丢数据 - #343

Merged
LUPENGHAN merged 4 commits into
1024XEngineer:mainfrom
LUPENGHAN:fix/realtime-batch-command-race
Aug 21, 2026
Merged

fix(realtime): 批量创建/删除日程时连续对话被强制挂断,且本地日历静默丢数据#343
LUPENGHAN merged 4 commits into
1024XEngineer:mainfrom
LUPENGHAN:fix/realtime-batch-command-race

Conversation

@LUPENGHAN

@LUPENGHAN LUPENGHAN commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

变更说明

  • QwenAudioSession.send_tool_result()(后端)不再在每次工具调用完就立刻抢发 response.create,而是延迟到该 response 自己的 response.done 到达之后再统一发一次。批量语音指令让模型在同一个 response 里连续调用多个工具时,不会再撞上 vendor "一个 session 同一时刻只能有一个 response 在跑" 的硬约束,也就不会再触发 "Cannot create response while another response is in progress." 报错、进而把连续对话强制挂断。
  • AssistantContinuousConversationService/AssistantConversationService(前端)收到 voice.command.result 时改用 commandResultChain 排队(跟同文件里 playbackChain/ExpoLocationMonitor.syncChain 一个模式),不再并发触发。批量操作下多条 voice.command.result 前后脚到达时,本地 SQLite 写入严格按顺序执行,不会再因为并发抢 withExclusiveTransactionAsync 的原生连接而互相踩锁、导致云端已提交的数据静默丢失。

Closes #341

测试计划

  • uv run pytest tests/infrastructure/external/realtime/ tests/intelligence/realtime -q --no-cov(后端,全绿)
  • npx tsc --noEmit(前端)
  • npx eslint(改动到的前端文件)
  • 针对两个 bug 各自的时序逻辑补了回归测试,并验证过测试本身能抓出问题:
    • 后端:模拟"同一个 response 里连续两次工具调用",断言 response.create 只在该 response 的 response.done 之后统一发一次(push-to-talk / 连续模式各一条)。
    • 前端:模拟"两条 voice.command.result 背靠背到达、第一条本地写入还没完成",断言第二条写入必须等第一条结束才能开始(两个 service 各一条)。
    • 验证方式:把对应的源码修复临时还原回旧逻辑,重新跑这四条新测试,全部按预期失败;改回修复后再全部转绿——证明测试确实在验证这次改动,不是摆设。
  • 真机人工验证一次批量语音创建/删除

LUPENGHAN and others added 2 commits August 21, 2026 14:53
… writes

Batch voice commands that call several tools within one realtime response
raced two ways: send_tool_result() asked the vendor for a follow-up reply
right after each tool call instead of waiting for that response's own
response.done, so a second in-batch tool call collided with the vendor's
"one response in flight" invariant and force-ended the call; and the
frontend applied each voice.command.result fire-and-forget, letting
concurrent local SQLite transactions race on withExclusiveTransactionAsync
and silently drop writes the cloud had already committed.

Fixes 1024XEngineer#341.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion

Both bugs fixed in 407ab61 only surface when a single turn triggers several
tool calls (batch create/delete). These tests exercise that shape directly
through the real service/session code:

- Two voice.command.result messages arriving back-to-back (no flush between
  them) must apply strictly in order; the second must not start until the
  first's transaction settles.
- Two tool calls landing in one still-open realtime response must produce
  exactly one deferred response.create, sent only after that response's own
  response.done.

Verified each test actually catches the regression by reverting its
corresponding source fix locally and confirming the new test fails, then
restoring the fix and confirming it passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LUPENGHAN
LUPENGHAN force-pushed the fix/realtime-batch-command-race branch from 21cb0a8 to 1c74885 Compare August 21, 2026 07:03
@LUPENGHAN
LUPENGHAN marked this pull request as ready for review August 21, 2026 07:07
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review conclusion

The realtime batch follow-up deferral is correct for multiple responding tools, and the frontend command-result chains correctly serialize local writes. One end-conversation path regresses turn settlement; see the inline finding. Focused validation reproduced it directly with the PR code.

The repository test dependencies are not installed in this workspace, so the full backend/frontend suites could not run.

Comment thread backend/src/timeflow/infrastructure/external/realtime/qwen_audio.py
LUPENGHAN and others added 2 commits August 21, 2026 15:19
response.done treated "a tool ran in this response" and "this response needs a
follow-up" as the same condition and kept looping either way. A tool that ends
the conversation asks for no follow-up (send_tool_result(..., respond=False)),
so that response.done is the turn's actual last event: continuing past it left
continuous mode never reporting turn_completed() (the call never hangs up) and
push-to-talk reading past the end of the stream and reporting a spurious
transport failure instead of settling cleanly.

Only continue when a follow-up was actually requested and not suppressed;
every other case -- including a suppressed one -- now falls through to the
normal settlement path.

Reported by the fennoai review bot on PR 1024XEngineer#343.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rite

commandResultChain used .then(onFulfilled, onRejected) to keep applying
queued voice.command.result writes after one fails. If the rejected branch
actually fires, that rejection sits unhandled until some later
queueCommandResult() call chains onto it -- and if there isn't one (e.g. it
was the last command result of the call), Node/Hermes treats it as an
unhandled rejection and crashes the process outright. Reproduced with a
state-subscriber listener that throws during markScheduleDataChanged()'s
notification.

Switch to .then(onFulfilled).catch(() => {}), the same idiom already used by
chainPlayback() in the same file: the .catch() is attached in the same
statement, so the rejection is neutralized immediately instead of waiting on
a future call that may never come.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review conclusion

The response-follow-up state machine now defers exactly one response.create until the originating response.done, including the suppressed end-conversation path, and the two frontend services serialize command-result writes while preserving ack-after-write behavior. I found no additional actionable correctness or regression issues in the complete fixed diff.

git diff --check passed. The focused backend tests could not run because uv is not installed in the workspace, and frontend tests could not run because frontend/node_modules is absent.

@LUPENGHAN
LUPENGHAN merged commit 0df8d07 into 1024XEngineer:main Aug 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(realtime): 批量创建/删除日程时连续对话被强制挂断,且本地日历静默丢数据

2 participants